Index: trunk/src/org/openstreetmap/josm/data/osm/FilterModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/FilterModel.java	(revision 12400)
+++ trunk/src/org/openstreetmap/josm/data/osm/FilterModel.java	(revision 12400)
@@ -0,0 +1,414 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.Graphics2D;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
+import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.widgets.OSDLabel;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * The model that is used both for auto and manual filters.
+ * @since 12400
+ */
+public class FilterModel {
+
+    /**
+     * number of primitives that are disabled but not hidden
+     */
+    private int disabledCount;
+    /**
+     * number of primitives that are disabled and hidden
+     */
+    private int disabledAndHiddenCount;
+    /**
+     * true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
+     */
+    private boolean changed;
+
+    /**
+     * Constructs a new {@code FilterTableModel}.
+     */
+    public FilterModel() {
+    }
+
+    private final transient List<Filter> filters = new LinkedList<>();
+    private final transient FilterMatcher filterMatcher = new FilterMatcher();
+
+    private void updateFilterMatcher() {
+        filterMatcher.reset();
+        for (Filter filter : filters) {
+            try {
+                filterMatcher.add(filter);
+            } catch (ParseError e) {
+                Main.error(e);
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        tr("<html>Error in filter <code>{0}</code>:<br>{1}",
+                                Utils.escapeReservedCharactersHTML(Utils.shortenString(filter.text, 80)),
+                                Utils.escapeReservedCharactersHTML(e.getMessage())),
+                        tr("Error in filter"),
+                        JOptionPane.ERROR_MESSAGE);
+                filter.enable = false;
+            }
+        }
+    }
+
+    /**
+     * Initializes the model from preferences.
+     * @param prefEntry preference key
+     */
+    public void loadPrefs(String prefEntry) {
+        List<FilterPreferenceEntry> entries = Main.pref.getListOfStructs(prefEntry, null, FilterPreferenceEntry.class);
+        if (entries != null) {
+            for (FilterPreferenceEntry e : entries) {
+                filters.add(new Filter(e));
+            }
+            updateFilterMatcher();
+        }
+    }
+
+    /**
+     * Saves the model to preferences.
+     * @param prefEntry preferences key
+     */
+    public void savePrefs(String prefEntry) {
+        Collection<FilterPreferenceEntry> entries = new ArrayList<>();
+        for (Filter flt : filters) {
+            entries.add(flt.getPreferenceEntry());
+        }
+        Main.pref.putListOfStructs(prefEntry, entries, FilterPreferenceEntry.class);
+    }
+
+    /**
+     * Runs the filters on the current edit data set.
+     */
+    public void executeFilters() {
+        DataSet ds = Main.getLayerManager().getEditDataSet();
+        changed = false;
+        if (ds == null) {
+            disabledAndHiddenCount = 0;
+            disabledCount = 0;
+            changed = true;
+        } else {
+            final Collection<OsmPrimitive> deselect = new HashSet<>();
+
+            ds.beginUpdate();
+            try {
+
+                final Collection<OsmPrimitive> all = ds.allNonDeletedCompletePrimitives();
+
+                changed = FilterWorker.executeFilters(all, filterMatcher);
+
+                disabledCount = 0;
+                disabledAndHiddenCount = 0;
+                // collect disabled and selected the primitives
+                for (OsmPrimitive osm : all) {
+                    if (osm.isDisabled()) {
+                        disabledCount++;
+                        if (osm.isSelected()) {
+                            deselect.add(osm);
+                        }
+                        if (osm.isDisabledAndHidden()) {
+                            disabledAndHiddenCount++;
+                        }
+                    }
+                }
+                disabledCount -= disabledAndHiddenCount;
+            } finally {
+                ds.endUpdate();
+            }
+
+            if (!deselect.isEmpty()) {
+                ds.clearSelection(deselect);
+            }
+        }
+        if (changed) {
+            updateMap();
+        }
+    }
+
+    /**
+     * Runs the filter on a list of primitives that are part of the edit data set.
+     * @param primitives The primitives
+     */
+    public void executeFilters(Collection<? extends OsmPrimitive> primitives) {
+        DataSet ds = Main.getLayerManager().getEditDataSet();
+        if (ds == null)
+            return;
+
+        changed = false;
+        List<OsmPrimitive> deselect = new ArrayList<>();
+
+        ds.beginUpdate();
+        try {
+            for (int i = 0; i < 2; i++) {
+                for (OsmPrimitive primitive: primitives) {
+
+                    if (i == 0 && primitive instanceof Node) {
+                        continue;
+                    }
+
+                    if (i == 1 && !(primitive instanceof Node)) {
+                        continue;
+                    }
+
+                    if (primitive.isDisabled()) {
+                        disabledCount--;
+                    }
+                    if (primitive.isDisabledAndHidden()) {
+                        disabledAndHiddenCount--;
+                    }
+                    changed |= FilterWorker.executeFilters(primitive, filterMatcher);
+                    if (primitive.isDisabled()) {
+                        disabledCount++;
+                    }
+                    if (primitive.isDisabledAndHidden()) {
+                        disabledAndHiddenCount++;
+                    }
+
+                    if (primitive.isSelected() && primitive.isDisabled()) {
+                        deselect.add(primitive);
+                    }
+                }
+            }
+        } finally {
+            ds.endUpdate();
+        }
+
+        if (!deselect.isEmpty()) {
+            ds.clearSelection(deselect);
+        }
+        if (changed) {
+            updateMap();
+        }
+    }
+
+    private static void updateMap() {
+        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
+        if (editLayer != null) {
+            editLayer.invalidate();
+        }
+    }
+
+    /**
+     * Clears all filtered flags from all primitives in the dataset
+     */
+    public void clearFilterFlags() {
+        DataSet ds = Main.getLayerManager().getEditDataSet();
+        if (ds != null) {
+            FilterWorker.clearFilterFlags(ds.allPrimitives());
+        }
+        disabledCount = 0;
+        disabledAndHiddenCount = 0;
+    }
+
+    /**
+     * Removes all filters from this model.
+     */
+    public void clearFilters() {
+        filters.clear();
+        updateFilterMatcher();
+    }
+
+    /**
+     * Adds a new filter to the filter list.
+     * @param filter The new filter
+     * @return true (as specified by {@link Collection#add})
+     */
+    public boolean addFilter(Filter filter) {
+        filters.add(filter);
+        updateFilterMatcher();
+        return true;
+    }
+
+    /**
+     * Moves down the filter in the given row.
+     * @param rowIndex The filter row
+     * @return true if the filter has been moved down
+     */
+    public boolean moveDownFilter(int rowIndex) {
+        if (rowIndex >= filters.size() - 1)
+            return false;
+        filters.add(rowIndex + 1, filters.remove(rowIndex));
+        updateFilterMatcher();
+        return true;
+    }
+
+    /**
+     * Moves up the filter in the given row
+     * @param rowIndex The filter row
+     * @return true if the filter has been moved up
+     */
+    public boolean moveUpFilter(int rowIndex) {
+        if (rowIndex == 0)
+            return false;
+        filters.add(rowIndex - 1, filters.remove(rowIndex));
+        updateFilterMatcher();
+        return true;
+    }
+
+    /**
+     * Removes the filter that is displayed in the given row
+     * @param rowIndex The index of the filter to remove
+     * @return the filter previously at the specified position
+     */
+    public Filter removeFilter(int rowIndex) {
+        Filter result = filters.remove(rowIndex);
+        updateFilterMatcher();
+        return result;
+    }
+
+    /**
+     * Sets/replaces the filter for a given row.
+     * @param rowIndex The row index
+     * @param filter The filter that should be placed in that row
+     * @return the filter previously at the specified position
+     */
+    public Filter setFilter(int rowIndex, Filter filter) {
+        Filter result = filters.set(rowIndex, filter);
+        updateFilterMatcher();
+        return result;
+    }
+
+    /**
+     * Gets the filter by row index
+     * @param rowIndex The row index
+     * @return The filter in that row
+     */
+    public Filter getFilter(int rowIndex) {
+        return filters.get(rowIndex);
+    }
+
+    /**
+     * Draws a text on the map display that indicates that filters are active.
+     * @param g The graphics to draw that text on.
+     * @param lblOSD On Screen Display label
+     * @param header The title to display at the beginning of OSD
+     * @param footer The message to display at the bottom of OSD. Must end by {@code </html>}
+     */
+    public void drawOSDText(Graphics2D g, OSDLabel lblOSD, String header, String footer) {
+        if (disabledCount == 0 && disabledAndHiddenCount == 0)
+            return;
+
+        String message = "<html>" + header;
+
+        if (disabledAndHiddenCount != 0) {
+            /* for correct i18n of plural forms - see #9110 */
+            message += trn("<p><b>{0}</b> object hidden", "<p><b>{0}</b> objects hidden", disabledAndHiddenCount, disabledAndHiddenCount);
+        }
+
+        if (disabledAndHiddenCount != 0 && disabledCount != 0) {
+            message += "<br>";
+        }
+
+        if (disabledCount != 0) {
+            /* for correct i18n of plural forms - see #9110 */
+            message += trn("<b>{0}</b> object disabled", "<b>{0}</b> objects disabled", disabledCount, disabledCount);
+        }
+
+        message += footer;
+
+        lblOSD.setText(message);
+        lblOSD.setSize(lblOSD.getPreferredSize());
+
+        int dx = Main.map.mapView.getWidth() - lblOSD.getPreferredSize().width - 15;
+        int dy = 15;
+        g.translate(dx, dy);
+        lblOSD.paintComponent(g);
+        g.translate(-dx, -dy);
+    }
+
+    /**
+     * Returns the list of filters.
+     * @return the list of filters
+     */
+    public List<Filter> getFilters() {
+        return new ArrayList<>(filters);
+    }
+
+    /**
+     * Returns the number of filters.
+     * @return the number of filters
+     */
+    public int getFiltersCount() {
+        return filters.size();
+    }
+
+    /**
+     * Returns the number of primitives that are disabled but not hidden.
+     * @return the number of primitives that are disabled but not hidden
+     */
+    public int getDisabledCount() {
+        return disabledCount;
+    }
+
+    /**
+     * Returns the number of primitives that are disabled and hidden.
+     * @return the number of primitives that are disabled and hidden
+     */
+    public int getDisabledAndHiddenCount() {
+        return disabledAndHiddenCount;
+    }
+
+    /**
+     * Determines if the filter state (normal / disabled / hidden) of any primitive has changed in the process.
+     * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
+     */
+    public boolean isChanged() {
+        return changed;
+    }
+
+    /**
+     * Returns the list of primitives whose filtering can be affected by change in primitive
+     * @param primitives list of primitives to check
+     * @return List of primitives whose filtering can be affected by change in source primitives
+     */
+    public static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) {
+        // Filters can use nested parent/child expression so complete tree is necessary
+        Set<OsmPrimitive> result = new HashSet<>();
+        Stack<OsmPrimitive> stack = new Stack<>();
+        stack.addAll(primitives);
+
+        while (!stack.isEmpty()) {
+            OsmPrimitive p = stack.pop();
+
+            if (result.contains(p)) {
+                continue;
+            }
+
+            result.add(p);
+
+            if (p instanceof Way) {
+                for (OsmPrimitive n: ((Way) p).getNodes()) {
+                    stack.push(n);
+                }
+            } else if (p instanceof Relation) {
+                for (RelationMember rm: ((Relation) p).getMembers()) {
+                    stack.push(rm.getMember());
+                }
+            }
+
+            for (OsmPrimitive ref: p.getReferrers()) {
+                stack.push(ref);
+            }
+        }
+
+        return result;
+    }
+}
