// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.openstreetmap.josm.actions.search.SearchAction.SearchMode; import org.openstreetmap.josm.actions.search.SearchCompiler; import org.openstreetmap.josm.actions.search.SearchCompiler.Match; import org.openstreetmap.josm.actions.search.SearchCompiler.Not; import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; import org.openstreetmap.josm.tools.SubclassFilteredCollection; /** * Class that encapsulates the filter logic, i.e. applies a list of * filters to a primitive. * * Uses {@link Match#match} to see if the filter expression matches, * cares for "inverted-flag" of the filters and combines the results of all active * filters. * * There are two major use cases: * * (1) Hide features that you don't like to edit but get in the way, e.g. * <code>landuse</code> or power lines. It is expected, that the inverted flag * if false for these kind of filters. * * (2) Highlight certain features, that are currently interesting and hide everything * else. This can be thought of as an improved search (Ctrl-F), where you can * continue editing and don't loose the current selection. It is expected that * the inverted flag of the filter is true in this case. * * In addition to the formal application of filter rules, some magic is applied * to (hopefully) match the expectations of the user: * * (1) non-inverted: When hiding a way, all its untagged nodes are hidden as well. * This avoids a "cloud of nodes", that normally isn't useful without the * corresponding way. * * (2) inverted: When displaying a way, we show all its nodes, although the * individual nodes do not match the filter expression. The reason is, that a * way without its nodes cannot be edited properly. * * Multipolygons and (untagged) member ways are handled in a similar way. */ public class FilterMatcher { /** * Describes quality of the filtering. * * Depending on the context, this can either refer to disabled or * to hidden primitives. * * The distinction is necessary, because untagged nodes should only * "inherit" their filter property from the parent way, when the * parent way is hidden (or disabled) "explicitly" (i.e. by a non-inverted * filter). This way, filters like * <code>["child type:way", inverted, Add]</code> show the * untagged way nodes, as intended. * * This information is only needed for ways and relations, so nodes are * either <code>NOT_FILTERED</code> or <code>PASSIV</code>. */ public enum FilterType { /** no filter applies */ NOT_FILTERED, /** at least one non-inverted filter applies */ EXPLICIT, /** at least one filter applies, but they are all inverted filters */ PASSIV } private static class FilterInfo { private final Match match; private final boolean isDelete; private final boolean isInverted; FilterInfo(Filter filter) throws ParseError { if (filter.mode == SearchMode.remove || filter.mode == SearchMode.in_selection) { isDelete = true; } else { isDelete = false; } Match compiled = SearchCompiler.compile(filter); this.match = filter.inverted ? new Not(compiled) : compiled; this.isInverted = filter.inverted; } } private final List<FilterInfo> hiddenFilters = new ArrayList<>(); private final List<FilterInfo> disabledFilters = new ArrayList<>(); /** * Clears the current filters, and adds the given filters * @param filters the filters to add * @throws ParseError if the search expression in one of the filters cannot be parsed */ public void update(Collection<Filter> filters) throws ParseError { reset(); for (Filter filter : filters) { add(filter); } } /** * Clears the filters in use. */ public void reset() { hiddenFilters.clear(); disabledFilters.clear(); } /** * Adds a filter to the currently used filters * @param filter the filter to add * @throws ParseError if the search expression in the filter cannot be parsed */ public void add(final Filter filter) throws ParseError { if (!filter.enable) { return; } FilterInfo fi = new FilterInfo(filter); if (fi.isDelete) { if (filter.hiding) { // Remove only hide flag hiddenFilters.add(fi); } else { // Remove both flags disabledFilters.add(fi); hiddenFilters.add(fi); } } else { if (filter.mode == SearchMode.replace && filter.hiding) { hiddenFilters.clear(); disabledFilters.clear(); } disabledFilters.add(fi); if (filter.hiding) { hiddenFilters.add(fi); } } } /** * Check if primitive is filtered. * @param primitive the primitive to check * @param hidden the minimum level required for the primitive to count as filtered * @return when hidden is true, returns whether the primitive is hidden * when hidden is false, returns whether the primitive is disabled or hidden */ private static boolean isFiltered(OsmPrimitive primitive, boolean hidden) { return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled(); } /** * Check if primitive is hidden explicitly. * Only used for ways and relations. * @param primitive the primitive to check * @param hidden the level where the check is performed * @return true, if at least one non-inverted filter applies to the primitive */ private static boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) { return hidden ? primitive.getHiddenType() : primitive.getDisabledType(); } /** * Check if all parent ways are filtered. * @param primitive the primitive to check * @param hidden parameter that indicates the minimum level of filtering: * true when objects need to be hidden to count as filtered and * false when it suffices to be disabled to count as filtered * @return true if (a) there is at least one parent way * (b) all parent ways are filtered at least at the level indicated by the * parameter <code>hidden</code> and * (c) at least one of the parent ways is explicitly filtered */ private static boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) { List<OsmPrimitive> refs = primitive.getReferrers(); boolean isExplicit = false; for (OsmPrimitive p: refs) { if (p instanceof Way) { if (!isFiltered(p, hidden)) return false; isExplicit |= isFilterExplicit(p, hidden); } } return isExplicit; } private static boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) { List<OsmPrimitive> refs = primitive.getReferrers(); for (OsmPrimitive p: refs) { if (p instanceof Way && !isFiltered(p, hidden)) return true; } return false; } private static boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) { boolean isExplicit = false; for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { if (!isFiltered(r, hidden)) return false; isExplicit |= isFilterExplicit(r, hidden); } return isExplicit; } private static boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) { for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { if (!isFiltered(r, hidden)) return true; } return false; } private static FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) { if (primitive.isIncomplete()) return FilterType.NOT_FILTERED; boolean filtered = false; // If the primitive is "explicitly" hidden by a non-inverted filter. // Only interesting for nodes. boolean explicitlyFiltered = false; for (FilterInfo fi: filters) { if (fi.isDelete) { if (filtered && fi.match.match(primitive)) { filtered = false; } } else { if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) { filtered = true; if (!fi.isInverted) { explicitlyFiltered = true; } } } } if (primitive instanceof Node) { if (filtered) { // If there is a parent way, that is not hidden, we show the // node anyway, unless there is no non-inverted filter that // applies to the node directly. if (explicitlyFiltered) return FilterType.PASSIV; else { if (oneParentWayNotFiltered(primitive, hidden)) return FilterType.NOT_FILTERED; else return FilterType.PASSIV; } } else { if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden)) // Technically not hidden by any filter, but we hide it anyway, if // it is untagged and all parent ways are hidden. return FilterType.PASSIV; else return FilterType.NOT_FILTERED; } } else if (primitive instanceof Way) { if (filtered) { if (explicitlyFiltered) return FilterType.EXPLICIT; else { if (oneParentMultipolygonNotFiltered(primitive, hidden)) return FilterType.NOT_FILTERED; else return FilterType.PASSIV; } } else { if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden)) return FilterType.EXPLICIT; else return FilterType.NOT_FILTERED; } } else { if (filtered) return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV; else return FilterType.NOT_FILTERED; } } /** * Check if primitive is hidden. * The filter flags for all parent objects must be set correctly, when * calling this method. * @param primitive the primitive * @return FilterType.NOT_FILTERED when primitive is not hidden; * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted * filter that applies; * FilterType.PASSIV when primitive is hidden and all filters that apply * are inverted */ public FilterType isHidden(OsmPrimitive primitive) { return test(hiddenFilters, primitive, true); } /** * Check if primitive is disabled. * The filter flags for all parent objects must be set correctly, when * calling this method. * @param primitive the primitive * @return FilterType.NOT_FILTERED when primitive is not disabled; * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted * filter that applies; * FilterType.PASSIV when primitive is disabled and all filters that apply * are inverted */ public FilterType isDisabled(OsmPrimitive primitive) { return test(disabledFilters, primitive, false); } }