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