001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Graphics2D;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.HashSet;
011import java.util.LinkedList;
012import java.util.List;
013import java.util.Set;
014import java.util.Stack;
015
016import javax.swing.JOptionPane;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.StructUtils;
020import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry;
021import org.openstreetmap.josm.data.osm.search.SearchParseError;
022import org.openstreetmap.josm.gui.MainApplication;
023import org.openstreetmap.josm.gui.widgets.OSDLabel;
024import org.openstreetmap.josm.spi.preferences.Config;
025import org.openstreetmap.josm.tools.Logging;
026import org.openstreetmap.josm.tools.Utils;
027
028/**
029 * The model that is used both for auto and manual filters.
030 * @since 12400
031 */
032public class FilterModel {
033
034    /**
035     * number of primitives that are disabled but not hidden
036     */
037    private int disabledCount;
038    /**
039     * number of primitives that are disabled and hidden
040     */
041    private int disabledAndHiddenCount;
042    /**
043     * true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
044     */
045    private boolean changed;
046
047    private final List<Filter> filters = new LinkedList<>();
048    private final FilterMatcher filterMatcher = new FilterMatcher();
049
050    private void updateFilterMatcher() {
051        filterMatcher.reset();
052        for (Filter filter : filters) {
053            try {
054                filterMatcher.add(filter);
055            } catch (SearchParseError e) {
056                Logging.error(e);
057                JOptionPane.showMessageDialog(
058                        Main.parent,
059                        tr("<html>Error in filter <code>{0}</code>:<br>{1}",
060                                Utils.escapeReservedCharactersHTML(Utils.shortenString(filter.text, 80)),
061                                Utils.escapeReservedCharactersHTML(e.getMessage())),
062                        tr("Error in filter"),
063                        JOptionPane.ERROR_MESSAGE);
064                filter.enable = false;
065            }
066        }
067    }
068
069    /**
070     * Initializes the model from preferences.
071     * @param prefEntry preference key
072     */
073    public void loadPrefs(String prefEntry) {
074        List<FilterPreferenceEntry> entries = StructUtils.getListOfStructs(
075                Config.getPref(), prefEntry, null, FilterPreferenceEntry.class);
076        if (entries != null) {
077            for (FilterPreferenceEntry e : entries) {
078                filters.add(new Filter(e));
079            }
080            updateFilterMatcher();
081        }
082    }
083
084    /**
085     * Saves the model to preferences.
086     * @param prefEntry preferences key
087     */
088    public void savePrefs(String prefEntry) {
089        Collection<FilterPreferenceEntry> entries = new ArrayList<>();
090        for (Filter flt : filters) {
091            entries.add(flt.getPreferenceEntry());
092        }
093        StructUtils.putListOfStructs(Config.getPref(), prefEntry, entries, FilterPreferenceEntry.class);
094    }
095
096    /**
097     * Runs the filters on the current edit data set.
098     */
099    public void executeFilters() {
100        DataSet ds = Main.main.getActiveDataSet();
101        changed = false;
102        if (ds == null) {
103            disabledAndHiddenCount = 0;
104            disabledCount = 0;
105            changed = true;
106        } else {
107            final Collection<OsmPrimitive> deselect = new HashSet<>();
108
109            ds.beginUpdate();
110            try {
111
112                final Collection<OsmPrimitive> all = ds.allNonDeletedCompletePrimitives();
113
114                changed = FilterWorker.executeFilters(all, filterMatcher);
115
116                disabledCount = 0;
117                disabledAndHiddenCount = 0;
118                // collect disabled and selected the primitives
119                for (OsmPrimitive osm : all) {
120                    if (osm.isDisabled()) {
121                        disabledCount++;
122                        if (osm.isSelected()) {
123                            deselect.add(osm);
124                        }
125                        if (osm.isDisabledAndHidden()) {
126                            disabledAndHiddenCount++;
127                        }
128                    }
129                }
130                disabledCount -= disabledAndHiddenCount;
131            } finally {
132                if (changed) {
133                    ds.fireFilterChanged();
134                }
135                ds.endUpdate();
136            }
137
138            if (!deselect.isEmpty()) {
139                ds.clearSelection(deselect);
140            }
141        }
142        if (changed) {
143            updateMap();
144        }
145    }
146
147    /**
148     * Runs the filter on a list of primitives that are part of the edit data set.
149     * @param primitives The primitives
150     */
151    public void executeFilters(Collection<? extends OsmPrimitive> primitives) {
152        DataSet ds = Main.main.getEditDataSet();
153        if (ds == null)
154            return;
155
156        changed = false;
157        List<OsmPrimitive> deselect = new ArrayList<>();
158
159        ds.beginUpdate();
160        try {
161            for (int i = 0; i < 2; i++) {
162                for (OsmPrimitive primitive: primitives) {
163
164                    if (i == 0 && primitive instanceof Node) {
165                        continue;
166                    }
167
168                    if (i == 1 && !(primitive instanceof Node)) {
169                        continue;
170                    }
171
172                    if (primitive.isDisabled()) {
173                        disabledCount--;
174                    }
175                    if (primitive.isDisabledAndHidden()) {
176                        disabledAndHiddenCount--;
177                    }
178                    changed |= FilterWorker.executeFilters(primitive, filterMatcher);
179                    if (primitive.isDisabled()) {
180                        disabledCount++;
181                    }
182                    if (primitive.isDisabledAndHidden()) {
183                        disabledAndHiddenCount++;
184                    }
185
186                    if (primitive.isSelected() && primitive.isDisabled()) {
187                        deselect.add(primitive);
188                    }
189                }
190            }
191        } finally {
192            ds.endUpdate();
193        }
194
195        if (!deselect.isEmpty()) {
196            ds.clearSelection(deselect);
197        }
198        if (changed) {
199            updateMap();
200        }
201    }
202
203    private static void updateMap() {
204        MainApplication.getLayerManager().invalidateEditLayer();
205    }
206
207    /**
208     * Clears all filtered flags from all primitives in the dataset
209     */
210    public void clearFilterFlags() {
211        DataSet ds = Main.main.getEditDataSet();
212        if (ds != null) {
213            FilterWorker.clearFilterFlags(ds.allPrimitives());
214        }
215        disabledCount = 0;
216        disabledAndHiddenCount = 0;
217    }
218
219    /**
220     * Removes all filters from this model.
221     */
222    public void clearFilters() {
223        filters.clear();
224        updateFilterMatcher();
225    }
226
227    /**
228     * Adds a new filter to the filter list.
229     * @param filter The new filter
230     * @return true (as specified by {@link Collection#add})
231     */
232    public boolean addFilter(Filter filter) {
233        filters.add(filter);
234        updateFilterMatcher();
235        return true;
236    }
237
238    /**
239     * Moves down the filter in the given row.
240     * @param rowIndex The filter row
241     * @return true if the filter has been moved down
242     */
243    public boolean moveDownFilter(int rowIndex) {
244        if (rowIndex >= filters.size() - 1)
245            return false;
246        filters.add(rowIndex + 1, filters.remove(rowIndex));
247        updateFilterMatcher();
248        return true;
249    }
250
251    /**
252     * Moves up the filter in the given row
253     * @param rowIndex The filter row
254     * @return true if the filter has been moved up
255     */
256    public boolean moveUpFilter(int rowIndex) {
257        if (rowIndex == 0)
258            return false;
259        filters.add(rowIndex - 1, filters.remove(rowIndex));
260        updateFilterMatcher();
261        return true;
262    }
263
264    /**
265     * Removes the filter that is displayed in the given row
266     * @param rowIndex The index of the filter to remove
267     * @return the filter previously at the specified position
268     */
269    public Filter removeFilter(int rowIndex) {
270        Filter result = filters.remove(rowIndex);
271        updateFilterMatcher();
272        return result;
273    }
274
275    /**
276     * Sets/replaces the filter for a given row.
277     * @param rowIndex The row index
278     * @param filter The filter that should be placed in that row
279     * @return the filter previously at the specified position
280     */
281    public Filter setFilter(int rowIndex, Filter filter) {
282        Filter result = filters.set(rowIndex, filter);
283        updateFilterMatcher();
284        return result;
285    }
286
287    /**
288     * Gets the filter by row index
289     * @param rowIndex The row index
290     * @return The filter in that row
291     */
292    public Filter getFilter(int rowIndex) {
293        return filters.get(rowIndex);
294    }
295
296    /**
297     * Draws a text on the map display that indicates that filters are active.
298     * @param g The graphics to draw that text on.
299     * @param lblOSD On Screen Display label
300     * @param header The title to display at the beginning of OSD
301     * @param footer The message to display at the bottom of OSD. Must end by {@code </html>}
302     */
303    public void drawOSDText(Graphics2D g, OSDLabel lblOSD, String header, String footer) {
304        if (disabledCount == 0 && disabledAndHiddenCount == 0)
305            return;
306
307        String message = "<html>" + header;
308
309        if (disabledAndHiddenCount != 0) {
310            /* for correct i18n of plural forms - see #9110 */
311            message += trn("<p><b>{0}</b> object hidden", "<p><b>{0}</b> objects hidden", disabledAndHiddenCount, disabledAndHiddenCount);
312        }
313
314        if (disabledAndHiddenCount != 0 && disabledCount != 0) {
315            message += "<br>";
316        }
317
318        if (disabledCount != 0) {
319            /* for correct i18n of plural forms - see #9110 */
320            message += trn("<b>{0}</b> object disabled", "<b>{0}</b> objects disabled", disabledCount, disabledCount);
321        }
322
323        message += footer;
324
325        lblOSD.setText(message);
326        lblOSD.setSize(lblOSD.getPreferredSize());
327
328        int dx = MainApplication.getMap().mapView.getWidth() - lblOSD.getPreferredSize().width - 15;
329        int dy = 15;
330        g.translate(dx, dy);
331        lblOSD.paintComponent(g);
332        g.translate(-dx, -dy);
333    }
334
335    /**
336     * Returns the list of filters.
337     * @return the list of filters
338     */
339    public List<Filter> getFilters() {
340        return new ArrayList<>(filters);
341    }
342
343    /**
344     * Returns the number of filters.
345     * @return the number of filters
346     */
347    public int getFiltersCount() {
348        return filters.size();
349    }
350
351    /**
352     * Returns the number of primitives that are disabled but not hidden.
353     * @return the number of primitives that are disabled but not hidden
354     */
355    public int getDisabledCount() {
356        return disabledCount;
357    }
358
359    /**
360     * Returns the number of primitives that are disabled and hidden.
361     * @return the number of primitives that are disabled and hidden
362     */
363    public int getDisabledAndHiddenCount() {
364        return disabledAndHiddenCount;
365    }
366
367    /**
368     * Determines if the filter state (normal / disabled / hidden) of any primitive has changed in the process.
369     * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
370     */
371    public boolean isChanged() {
372        return changed;
373    }
374
375    /**
376     * Returns the list of primitives whose filtering can be affected by change in primitive
377     * @param primitives list of primitives to check
378     * @return List of primitives whose filtering can be affected by change in source primitives
379     */
380    public static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) {
381        // Filters can use nested parent/child expression so complete tree is necessary
382        Set<OsmPrimitive> result = new HashSet<>();
383        Stack<OsmPrimitive> stack = new Stack<>();
384        stack.addAll(primitives);
385
386        while (!stack.isEmpty()) {
387            OsmPrimitive p = stack.pop();
388
389            if (result.contains(p)) {
390                continue;
391            }
392
393            result.add(p);
394
395            if (p instanceof Way) {
396                for (OsmPrimitive n: ((Way) p).getNodes()) {
397                    stack.push(n);
398                }
399            } else if (p instanceof Relation) {
400                for (RelationMember rm: ((Relation) p).getMembers()) {
401                    stack.push(rm.getMember());
402                }
403            }
404
405            for (OsmPrimitive ref: p.getReferrers()) {
406                stack.push(ref);
407            }
408        }
409
410        return result;
411    }
412}